Separation of schema migration and deploy
前提
RDBを利用するアプリケーションの話
2024/04追記
小中規模のアプリケーションはデプロイパイプラインの中でschema migrationを行う
大規模のアプリケーションになると
大量データのあるテーブルにDDLを流すのが危険になっていく
大規模チームが頻繁にデプロイするとお互いの変更でブロックされて調整コストが高くなる 1. 同時派
データベースのスキーマ変更が怖いのは、異なる手順でやっているからだ
アプリケーションと同じパイプラインでやれば手作業はなくなるし、アプリケーションのデプロイ同様に追跡可能になる
データベースマイグレーションを特別視せず、当たり前のContinuous Deliveryプロセスに組み込む
同時に実行したとしても個別に実行できるようにはしておく
ohbarye.icon
ちょっと古いんじゃないかと思う
「DBAとアプリケーションエンジニアが別でDBAが最後の砦」みたいな世界観に対して語っている
「データベースの変更はスクリプト化してバージョン管理せよ」
ohbarye.icon
Rails関連では同時がふつうだからか、「同時にやるべきだ」という主張の記事はあまりなかった
capistranoのような定番ツールがサポートしているから、というのもありそう
昔ながらの開発は分離されていたはず
DDLを書いてDBに対して実行して、終わったらアプリケーションをデプロイして〜という流れ
長所
schema migrationを行うタイミングを開発者が気にしなくて良い(本当に利点か?という指摘はある)
schema migrationし忘れることなくデプロイできる
migration対象の環境が増えると大事になってくる
短所 => 分離派の意見を参照
2. 分離派
GOV.UK Government Digital Service(イギリスのデジタル庁みたいな)に勤める @bruntonspall Every change you make must be backward compatible with the rest of the system
手数も増えるしワークフローを整備するのも大変だがそれに見合うメリットがある
ソフトウェアのデプロイ・リリースをシンプルに保つことの重要性
デプロイがシンプルになる=サイクルタイムが短くなる=スループットが速くなる
ローリングアップデートを可能にし、リリースプロセスのコントローラビリティが増す
分離によるスループットの向上は早期にリターンがある
データベースのマイグレーションは、コードの変更よりも本質的にリスクの高い種類の変更である
データベースの変更はコードの変更よりもロールバックが難しい
デプロイ後にマイグレーションを非同期で実行するアイデア
互換性のないデータベーススキーマの実行は最悪の結果を引き起こす
マイグレーションはデプロイの前と後どちらでも行えるようにする
危険なマイグレーションの遅延実行を強制する
アプリケーションとDBはライフサイクルが異なる
アプリケーションが互換性を担保しなければならない
その他
table rebuildが発生するようなmigration
ohbarye.icon
両者を厳密に同時にアトミックに変更することはできない スキーマ変更が完了した瞬間にアプリケーションのデプロイが終わる or デプロイされたインスタンスにリクエストを振り分けるのは不可能に近い
参考にするのはマイクロサービスでもいいし、モバイルアプリのバックエンドAPIでもいい スキーマ変更は常に前方互換でなければ、アプリケーションがクラッシュする可能性がある
スキーマ変更を前方互換にするため、アプリケーション側が両バージョンに対応できるよう柔軟性を持たせなければならない
DBを賢く作るよりアプリケーションを賢く作るほうが容易
複数のコードベースを持つものは分散システムである
If there are multiple codebases, it’s not an app – it’s a distributed system.
アプリケーションの実行のステージでは可変部分を持たないようにする
実行のステージ=プロセスを起動してアプリケーションを実行環境の中で実行する
アプリケーションの実行を妨げるような問題が起きると、開発者が待機していない真夜中にアプリケーションが壊れる結果になる
3. 分離せざるを得ない派
モノリスのDBはridgepole gemによりmigrationを行うよう分離されている 人間がマイグレーションの実行タイミングをデプロイとは別で決めている
ゼロダウンタイムのためではなく巨大なモノリスを数十人で開発するためスキーマ管理が難しいという背景 互いの作業をブロックしあってしまう
長期的にはマイクロサービスにより基本に立ち戻る雰囲気
ALTER TABLEの実行時間が長い例も挙げられている
複数のプロジェクトが同一のDBを利用する
システム規模別
歴史の浅い小規模なRailsアプリ => デプロイ頻度も多いのであえてmigrationを分けると手間。デプロイのタイミングでマイグレーションも走らせる。
歴史の長い大規模なRailsアプリ => データ量の多いテーブルに関するmigrationはレビュー等で注意して、必要に応じてメンテなど調整の上migrationだけ個別にメインブランチにマージしてデプロイ。それ以外のものは小規模Railsアプリと同じ。
DBを他のアプリと共有しているRailsアプリ => DB変更が他のアプリに影響を与える可能性があるので、デプロイとは別のタイミングに実施。DB変更用のリポジトリ作ってRailsのマイグレーション使ってないケースもあった(正直辛かった)
by suusan2go
パターン
「システムコンポーネントに変更を加えるときは、対象以外にとって破壊的な変更を入れてはいけない」というのを原則とすると、基本的には前方互換性のある変更しか加えてはならないことになる
対象に関連するシステムコンポーネント側の変更により互換性を獲得できるようになる
DBでremove columnするのは、利用するアプリケーションに対して前方互換性がない
しかしアプリケーション側でcolumnの参照を消しきったあとであればremove columnも前方互換となる
アプリケーションとDBの連携には4パターンある
1. old app -> old DB schema
2. old app -> new DB schema
3. new app -> old DB schema
4. new app -> new DB schema
「同時に行う」というのが、2と3を隠して1か4しか存在しないと誤認させるのだとしたら悪いアイデア
2はnew DB schemaに前方互換性がないとクラッシュする可能性がある
3はnew DB schemaに依存したコードがappに含まれているときにクラッシュする可能性がある
厳密にアトミックに行うことはできないため、ダウンタイムを設けない場合2と3は存在する
けどみんなそんなにちゃんとFeature Toggleできているのか?とは思う
appとDBが1:1のようなシンプルな世界観ならよい
だが現実的にはappもDBも少なからず複数台構成であり、以下のようなタイミングで2や3が発生している
複数台のDBにマイグレーションを順次行っている間
カナリーの100%展開までを15分、としてもその間に2 or 3のためのクラッシュを引き起こすリクエストが来ない保証はない
にも関わらず1か4しかテストされない問題
自動テストはどちらかしかテストしない
手動テストはデプロイが完了したらテストを始めている
もしかすると2, 3は10%どころか0.1%なのかもしれない
それを許容できるかどうかは事業・製品・チーム次第
すべてのシステムがゼロダウンタイムを目指す必要はない
複数バージョンがほぼ絶対に共存する世界
いずれにせよ「2 バージョンをバツンと切り換える」というのはとてもやりづらくなっている(コントロールプレーン任せ)ので、2 バージョンが共存しても大丈夫な状態を維持しながらデプロイするという考え方により強くシフトする必要がある。
例えば nullable で add_column しておくとか、暗号化キーを変えたときに、暗号文字列を新旧両方のキーで復号を試すとか。前方互換性と後方互換性の両方が必要。
ohbarye.icon 感想
いにしえの開発では分離されていたはず
Rails以外での開発を過去に経験した人の多くは「そういえば昔<任意の言語>で開発してたときは分離してた」と言う
Rails界隈ではcapistranoのようなデプロイとマイグレーションを同時に行えるデプロイツールが広く普及していることもあり、「同時がふつう」と言えるまでになっている(ように見える)
SimpleとEasyの話にも関わる
同時に行うフローはRailsが志向するEasyと相性が良いと思う
ここでいうEasyはMomentum builder (弾みを付けるもの) であり、8~9割のことをうまく解決してくれるもの
プロダクトの機能開発に集中できる体験を生み出す
Simpleを志向しようとすると分離が魅力的になる
デプロイとマイグレーションを密にすることはお互いのSimplicityを損なうことになる
「ふつう」とは何か
共通認識のもとでEasyになっていること
アプリケーションのバージョン管理とデータベースのバージョン管理が統合されており、サーバ・クライアントの依存関係のはずなのに主従が逆転している
アプリケーション優位になり、アプリケーション視点でのデプロイに合わせてデータベースを変更するようになった
統合すること自体はベストプラクティスとして知られるようになった
DBAとアプリケーションエンジニアの分離は過去のものになりつつある?
参考
「複数バージョンがほぼ絶対に共存する世界」というのが刺さった
そういう世界観になったときに、これまでの「ふつう」が通用するのかどうか改めて考えるきっかけになった
デプロイ戦略がものすごくまとまっている
データストアの考慮がないというコメントがあったので、まさにこの記事がそこを深堀りする感じだと良い
Googleには、複雑なデプロイメントに対応するためのロールアウト自動化フレームワークがある 単純にも複雑にもできる
デプロイにはオーケストレーションが必要
データベースマイグレーションのようなタスクを行いつつ、複数のサービスを決まった順序でデプロイしないといけない
In other words, you need to deploy multiple services together in a particular order, while you perform other tasks such as database migrations in strict synchronization.
オーケストレーションの必要をなくし、サービスを独立してデプロイできるようにすることを目標とする
それぞれのサービスが下位互換性を持つようにする慎重な設計が必要
サービスのクライアントを決まった手順で更新する必要がなくなり、後で独立に更新できるようになる
優れたコミュニケーションと包括的な構成管理がデータベースの課題に関連する
すべての変更をバージョン管理において保持されているmigration scriptとしてキャプチャする
migrationの進行状況をトラッキングできるようにする
ダウンタイムをなくすには
これらのツールでは、変更する各テーブルの「ゴースト」コピーを作成し、空のコピーを移行してから、移行中に発生する更新を含むデータを元のテーブルから段階的にコピーします。この処理が完了すると、元のテーブルはゴーストに置き換えられます。Cloud Spanner などの一部のデータベースでは、ダウンタイムなしでスキーマ更新を実行できます。
本番環境に似たデータセット(すべての個人情報や機密情報がスクラブされた状態)に対するすべてのスキーマ変更をテストし、アプリケーションが移行中と移行後に想定通りに動作していることを確認する
この目的での使用のために、本番環境データベースのスクラブされたバージョンを毎日作成することもある
ruby-jpでのアンケートログ
ohbarye:github_octocat: 6:25 PM
ちょっと古いですがShopifyのデプロイに関する記事を読んでいて「多くの会社ではデプロイの一部としてDB migrationをやってる」的な記述があり、オッそうなの?と思ったのですが、皆さんの関わるRails applicationではどんな運用されてますか?
While running migrations as a part of the deploy is the default approach used by most of companies, for some reason Rails community never reconsidered alternatives.
Kir ShatrovKir Shatrov
Asynchronous Active Record migrations
My personal blog, mostly about technical staff.
Apr 1st, 2018
VoteAPP 6:25 PM
deployとDB migrationのやり方 (Choose one) (edited)
Results (40 voters):
███████████ (28) 同時にやってる
███ (9) 別々にやってる
(1) その他のやり方
(1) Abstain
ohbarye:github_octocat: 6:41 PM
そのやり方を選んだ理由も気になります… :eyes:
ohbarye:github_octocat: 7:01 PM
先に自分の意見を書くと別々にやるのが好みです。
アプリケーションのdeploymentとDB migrationの間になるべく依存関係を作りたくない、というのが主な理由です(悪い依存関係の例: DB migrationが完了しないとdeploymentが行えない。deployしたいアプリケーションコードの変更と関係ないDB migrationが暗黙的に実行される)
ybiquitous 9:13 PM
migrationによってはtable rebuildが発生して時間がかかることがあるので、別々で実行してます。
chiastolite 9:22 PM
カラムの追加とかあったとき、コードだけ新しくするとエラーとか起きちゃうのはどう対処する感じですか?
9:23
(昔こういう話はよくやられてたけど今はあんまりされてない感はある)
ybiquitous 9:26 PM
先に db/**/*.rb だけ含んでデプロイ& rails db:migrate しておいて、あとで他のコードをデプロイしてますねー。
削除の場合は逆に、消すattributeの参照を全部消してデプロイ→ ignored_colums セットしてデプロイ→ db/ だけデプロイ& db:migrate → ignored_columns 消す
みたいにしてます。
chiastolite 9:33 PM
なるほど。だとリリース時は順々にブランチを取り込んだりする感じですかね?それか一連の流れがデプロイパイプラインとして整ってるか (edited)
ybiquitous 9:35 PM
そうですね。ブランチ分けて順々にデプロイしてます。
genya0407 1:00 AM
「別々にやる」というのは、「今日デプロイするぞ〜」という日の中で、まずmigrationを流して、終わったらアプリをデプロイする、というのも含まれるんですかね
:hai-:
1
ybiquitous 8:54 AM
そうですね。時間空けないですぐにやってます。
genya0407 10:37 AM
そうなると「同時にやる」ってなんでしょうね。デプロイコマンドの中にmigrationが含まれるかどうか…?
ohbarye:github_octocat: 10:41 AM
ですです。migration -> deployの一連の流れをデプロイパイプラインやデプロイスクリプトで組んでいて、間に人が介在しないようなやり方を「同時にやる」と表現してました (edited)
:alembic:
1
:naruhodo:
3
ujihisa:kakko-kabu-tojikakko: 10:44 AM
現職は実はMongoDBというものをMongoMapperというもの経由で触っている関係で、indexを張るなどの作業は別々にやってるんですが、
10:44
基本的にこれまでRDBを使ういわゆるふつうのrailsのmigration使うときは、同時にやってきました
10:46
理由
* webアプリのコードとDBスキーマはそもそも不可分である。(アプリケーションのdeploymentとDB migrationの間の依存関係は必然とみなす)
* コードのデプロイと、(ユーザからみえる)機能のリリースはそもそもDarklaunchなどのFeature togglesで分離しているので、↑の依存をそれほど問題視しない
genya0407 6:21 PM
ujihisaさんのおっしゃるとおり、スキーマ変更とアプリの改修は不可分だと思うので、migrationとdeployを同時にやらないことでどのようなメリットがあるのかあまりわかりませんでした…
tsugimoto 11:40 PM
必要だからスキーマ変更をマージしているのだから、次のデプロイでそれが migration されるのは致し方なしなのかなという感覚でした(次の本番デプロイで走って困る migration は feature ブランチとかに隔離しておく発想)
ohbarye:github_octocat: 12:01 AM
スキーマ変更とアプリの改修は不可分
なるほどー!自分はDBの変更とアプリの改修は概ね不可分だがそうでないこともある、ぐらいの認識でした。
migrationだけすれば良い例
インデックスや制約を作成/削除する
(アプリからすでに利用されない) 古いcolumn/tableを消す
アプリだけデプロイすれば良い例
実行されるmigrationと関係ない変更をデプロイする
「必要だからスキーマ変更をマージしている」のが殆どのケースだと思いますが、分離することで
片方だけをやりたいケースに対応できる
走って困るmigrationをfeatureブランチに隔離させておく、という開発者のbranch管理コストが下がる
あたりがメリットかなと思っています。
12:04
:thought_balloon: 別々に実行するためにかかる人的コストも当然あるが…
chiastolite 12:14 AM
migrationとデプロイを分けると、今のサーバーの状態をどうにか把握しておく必要が出てきてそのうち反映されるべきmigrationが実行されていないとかでトラブルとかありそう (edited)
:wakaru:
1
:sorena:
1
12:16
migrationとデプロイを分けるとしたら、どちらかというとインフラ要因でやりそう
koheisg 12:17 AM
1. migrationのみデプロイ
2. 初期データを投入 (バッチとか。migrationに書いてしまうプロジェクトもある)
3. migrationの変更を利用する機能のリリース
とか毎日のようにやってます
それぞれマージされたら実行されるような感じもあるし、chatopsやデプロイツール(jenkinsやrundeckなど)でやることもありますね。 (edited)
genya0407 12:20 AM
走って困るmigrationが入ったブランチをデプロイするのこわい
koheisg 12:22 AM
それって、メソッドシグネチャのrename/削除とかもですかね? (edited)
ohbarye:github_octocat: 12:24 AM
どちらかというとインフラ要因でやりそう
Shopifyのブログだと「シャーディングしてたらそれぞれのDBにmigrationしなければいけない」「migrationに時間かかったらそのぶんデプロイがブロックされる」(たぶんmigrationに依存してないデプロイのことを言ってる)みたいな記述がありました :memo:
koheisg 12:25 AM
シャーディングしてるようなDBにmigration当てる話だったら、絶対に同時にデプロイとか厳しいですよね。。
:ariuru:
1
:eyes:
1
11 replies
Last reply 12 days agoView thread
koheisg 12:27 AM
そもそもpt-online schema changeとか使わないとだめなんでしょうけど、うまくいった試しがないので、土下座してメンテインしてる。。(誰か教えてください。。。 (edited)
12:30
(なので、規模によるので、データ量とアクセス数によってmigration分けたり、アプリと同時にやってたりいろいろなのでその他に入れました。)
:wakarite:
1
ohbarye:github_octocat: 12:34 AM
deployとmigrationの分離の話、どこかの本で見たよな…と思って探してたら『継続的デリバリー』に記述がありました(ようやく見つけた
ダウンタイムをゼロにするためのアプローチの1つって感じで紹介されているぽい
ohbarye:github_octocat: 12:40 AM
(自分も好みは書きましたがどちらが正解とかは考えていなくて、チームや規模に基づいて生じるトレードオフがあるよな〜と思いつつ、どのあたりが判断基準なのかを手探っているので皆さんの状況も知りたい!というお気持ちです)
chiastolite 12:40 AM
クラウド時代だから、
旧DBをクローン(新DBとする)
新DBにmigration適用(データの更新なども)
新アプリをローリングデプロイしてこっちは新DBを見る
旧アプリのアクセスでDBに更新があるとトリガーとかで新DBに都度反映(pt-online schema changeみたいなの)
新アプリが撒き終わったら旧アプリと旧DBを削除
(edited)
12:41
みたいなのが何も考えずバーンとやれて欲しい
:顧客が本当に必要だったもの:
2
:soreda:
2
koheisg 12:42 AM
スキーマ変更を伴うカナリーリリースとかしたい。。
猫宮みみみ 9:35 PM
自分も基本同時派(デプロイコマンドを一発打てば、マイグレーションや初期化処理、
バッチ等を含め、全依存タスクが自動で完了するのが好き)なんですが、
ダウンタイムをゼロにするためのアプローチの1つ
というのは、分離で得られるメリットですね。
昔ながらの技術を使った例になりますが、
アプリから参照されないレプリカに対して、長時間の alter (migration) を実施
migration完了後に、フェイルオーバーを発生させ、プライマリに昇格 (ダウンタイム数秒)
旧アプリからのアクセス時、新カラム等はDB側の初期値で初期化
全レプリカを、同様に、一つずつサービスから切り離しつつ、migrationを浸透させる
新スキーマになった状態で、新アプリをdeploy
deploy の複雑化や工程/時間増のデメリットが非常に大きいので、
ここぞというタイミング以外は同時deployしてましたが。
:ariuru:
1
ohbarye:github_octocat: 9:53 PM
同時派の中でも
アプリケーションにmigration前のDBに対しての前方互換性を持たせる派(migrationがコケてもデプロイできるよ派)
持たせない派(migrationが終わらないとdeployさせないよ派)
がいることがわかってきました
suusan2go 10:55 PM
今まで自分が遭遇したケースだとこんな感じでしたね
歴史の浅い小規模なRailsアプリ => デプロイ頻度も多いのであえてmigrationを分けると手間。デプロイのタイミングでマイグレーションも走らせる。
歴史の長い大規模なRailsアプリ => データ量の多いテーブルに関するmigrationはレビュー等で注意して、必要に応じてメンテなど調整の上migrationだけ個別にメインブランチにマージしてデプロイ。それ以外のものは小規模Railsアプリと同じ。
DBを他のアプリと共有しているRailsアプリ => DB変更が他のアプリに影響を与える可能性があるので、デプロイとは別のタイミングに実施。DB変更用のリポジトリ作ってRailsのマイグレーション使ってないケースもあった(正直辛かった)